﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>Console.Write("");</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class StatementHasNoEffectDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        public const string DiagnosticId = "StatementHasNoEffect";

        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            DiagnosticId,
            "Statement has no effect",
            "Statement can be removed, because it has no effect",
            "Performance",
            DiagnosticSeverity.Warning
        );

        public virtual ImmutableArray<SyntaxKind> SyntaxKindsOfInterest { get { return ImmutableArray.Create(SyntaxKind.ExpressionStatement); } }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public virtual void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var expression = node as ExpressionStatementSyntax) != null)
            if ((var invocation = expression.Expression as InvocationExpressionSyntax) != null)
            if ((var method = semanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol) != null)
            {
                // Console.Write("")
                if (IsWriteEmptyString(invocation, method, semanticModel, cancellationToken))
                    addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
            }
        }

        private bool IsZeroConstantValue(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
        {
            var constantValue = semanticModel.GetConstantValue(expression, cancellationToken);

            return constantValue.HasValue && constantValue.Value.Equals(0);
        }

        private bool IsEmptyStringConstantValue(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
        {
            var constantValue = semanticModel.GetConstantValue(expression, cancellationToken);

            return constantValue.HasValue && constantValue.Value.Equals(String.Empty);
        }

        private bool IsNullConstantValue(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
        {
            var constantValue = semanticModel.GetConstantValue(expression, cancellationToken);
            return constantValue.HasValue && constantValue.Value.Equals(null);
        }

        private bool IsWriteEmptyString(InvocationExpressionSyntax invocation, IMethodSymbol method, SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            return method.Name == "Write" &&
                (method.ContainingType.Equals(semanticModel.Compilation.GetTypeByMetadataName(typeof(Console).FullName)) || method.OriginalDefinition.ContainingType.Equals(semanticModel.Compilation.GetTypeByMetadataName(typeof(System.IO.TextWriter).FullName))) &&
                invocation.ArgumentList.Arguments.Count == 1 &&
                IsEmptyStringConstantValue(semanticModel, invocation.ArgumentList.Arguments.Single().Expression, cancellationToken)
            ;
        }
    }

    [ExportCodeFixProvider(StatementHasNoEffectDiagnosticAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class StatementHasNoEffectCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            return new[] { StatementHasNoEffectDiagnosticAnalyzer.DiagnosticId };
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);

            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // Find the local declaration identified by the diagnostic.
            var invocation = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ExpressionStatementSyntax>().First();

            // return a code action that will invoke the fix.
            return new[] { CodeAction.Create("Remove statement", c => Fix(document, invocation, c)) };
        }

        private async Task<Document> Fix(Document document, ExpressionStatementSyntax invocation, CancellationToken cancellationToken)
        {
            var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken);

            var newRoot = syntaxRoot.RemoveNode(invocation, default(SyntaxRemoveOptions));

            return document.WithSyntaxRoot(newRoot);
        }
    }
}
